package service

import (
	"context"
	"encoding/json"
	"fmt"
	"inventory/api/inventory"
	"inventory/internal/constant"
	"inventory/internal/model"
	"inventory/internal/pkg/generator"
	"inventory/pkg/ecode"
	"inventory/pkg/orm"
	"inventory/pkg/util"
	"strconv"
	"strings"
	"time"
)

// GetLossList 获取报损记录
func (s *Service) GetLossList(ctx context.Context, req *inventory.GetLossListRequest) (resp *inventory.GetLossListResponse, err error) {
	var (
		views                       []*inventory.LossView
		lossDetails, subLossDetails []*model.LossDetail
		total                       int64
		startDate, endDate          time.Time
	)

	empId := req.WebToken.EmpId
	if startDate, endDate, err = verifyStartDateAndEndDate(req.StartDate, req.EndDate); err != nil {
		return
	}

	if _, err = s.CheckUsagePermission(ctx, empId, req.PStoreId, req.Id, constant.VisitConsumptionOrder); err != nil {
		return
	}
	if total, lossDetails, err = s.dao.FindLossOrderDetails(
		ctx, int(req.Id), int(req.PageNo), int(req.PageSize), startDate, endDate, req.QueryKeywords); err != nil {
		return
	}
	orderIDs := make([]uint64, len(lossDetails))
	for i, d := range lossDetails {
		orderIDs[i] = d.OrderID
	}
	if subLossDetails, err = s.dao.FindSubLossOrderDetails(ctx, orderIDs); err != nil {
		return
	}
	subMap := make(map[uint64][]*model.LossDetail)
	for _, d := range subLossDetails {
		subMap[d.ParentID] = append(subMap[d.ParentID], d)
	}
	views = make([]*inventory.LossView, len(lossDetails))
	for i, d := range lossDetails {
		views[i] = &inventory.LossView{
			CreateTime:  d.CreateTime.Format(util.DefaultLayout),
			StorageType: inventory.StorageType(d.StorageType),
			StorageKey:  d.StorageKey,
			Quantity:    d.Quantity,
			Remark:      d.Remark,
			Sub:         []*inventory.SubLossView{},
		}
		if subs, ok := subMap[d.ID]; ok {
			subViews := make([]*inventory.SubLossView, len(subs))
			for j, sub := range subs {
				sr := model.LossSnapShotRemark{}
				if err = json.Unmarshal(sub.Remark, &sr); err != nil {
					return
				}
				subViews[j] = &inventory.SubLossView{
					Quantity:     sub.Quantity,
					Name:         sr.Name,
					Code:         sr.Code,
					Unit:         sr.Unit,
					Type:         sr.Type,
					StorageUnit:  sr.StorageUnit,
					StorageRatio: sr.StorageRatio,
				}
			}
			views[i].Sub = subViews
		}
	}
	resp = &inventory.GetLossListResponse{
		Details: views,
		Total:   uint32(total),
	}
	return
}

// AddLossInfo 添加报损单
func (s *Service) AddLossInfo(ctx context.Context, req *inventory.AddLossInfoRequest) (resp *inventory.DefaultResp, err error) {
	var (
		orderNumber                    string
		objectRemarks, materialRemarks map[string]*model.FullRemark
		warehouse                      *model.Warehouse
		storages                       []*model.Storage
	)
	empId, sgId := req.WebToken.EmpId, req.WebToken.SgId
	if len(req.Details) == 0 {
		err = ecode.BadRequest("明细数量不允许为0")
		return
	}
	objectKeyMap := make(util.StringBoolMap)
	storageKeyMap := make(util.StringBoolMap)
	materialIDMap := make(util.UInt64BoolMap)
	bomMap := make(map[string]map[string]float64)
	for _, d := range req.Details {
		if d.Quantity <= 0 {
			err = ecode.BadRequest("损耗数量必须大于0")
			return
		}
		if _, ok := storageKeyMap[d.StorageKey]; ok {
			err = ecode.BadRequest("商品Storage_key重复")
			return
		}
		storageKeyMap[d.StorageKey] = true
		switch d.StorageType {
		case inventory.StorageType_MATERIAL:
			var materialID int
			if materialID, err = strconv.Atoi(strings.Split(d.StorageKey, "-")[1]); err != nil {
				err = ecode.BadRequest("物料Storage_key格式错误")
				return
			}
			materialIDMap[uint64(materialID)] = true
		default:
			objectKeyMap[d.StorageKey] = true
		}
	}
	if len(objectKeyMap) != 0 {
		if objectRemarks, err = s.downstream.BuildObjectRemarks(ctx, sgId, objectKeyMap.GetKeys()); err != nil {
			return
		}
		if len(objectRemarks) != len(objectKeyMap) {
			err = ecode.BadRequest("请求中存在无效的商品SKU")
			return
		}
		for _, o := range objectRemarks {
			if req.PStoreId != 0 && o.PStoreID != 0 && req.PStoreId != o.PStoreID {
				err = ecode.BadRequest("当前所选商品不可用")
				return
			}
		}
		var productBoms []*model.BomConfig
		if productBoms, err = s.dao.FindBomByStorageKeys(ctx, storageKeyMap.GetKeys()); err != nil {
			return
		}
		for _, bom := range productBoms {
			if _, ok := bomMap[bom.StorageKey]; !ok {
				bomMap[bom.StorageKey] = make(map[string]float64)
			}
			bomMap[bom.StorageKey][fmt.Sprintf("m-%d", bom.MaterialID)] = bom.Quantity
			materialIDMap[bom.MaterialID] = true
		}
	}
	if len(materialIDMap) != 0 {
		if materialRemarks, err = s.dao.BuildMaterialsRemarks(ctx, sgId, materialIDMap.GetKeys()); err != nil {
			return
		}
		if len(materialRemarks) != len(materialIDMap) {
			err = ecode.BadRequest("请求中存在无效的产品ID")
			return
		}
		for _, o := range materialRemarks {
			if req.PStoreId != 0 && o.PStoreID != 0 && req.PStoreId != o.PStoreID {
				err = ecode.BadRequest("当前所选产品不可用")
				return
			}
		}
	}
	resp = &inventory.DefaultResp{
		Status: true,
	}
	err = s.dao.Transaction(ctx, func(ctx context.Context) (err error) {
		if warehouse, err = s.CheckUsagePermission(ctx, empId, req.PStoreId, req.Id, constant.CreateConsumptionOrder); err != nil {
			return
		}
		if warehouse.StorageMode == constant.WarehouseStorageModeStrong {
			// 强管控模式，需要判定报损数量不超过库存数量
			if storages, err = s.dao.FindWarehouseStoragesByStorageKeys(ctx, req.Id, storageKeyMap.GetKeys()); err != nil {
				return
			}
			storageMap := make(model.StorageMap)
			storageMap.InitStorageMap(storages)
			for _, d := range req.Details {
				if d.StorageType != inventory.StorageType_MATERIAL {
					if materialRefMap, ok := bomMap[d.StorageKey]; ok {
						for materialKey, refQuantity := range materialRefMap {
							consumeQuantity := d.Quantity * refQuantity
							if storage, hasStorage := storageMap[materialKey]; !hasStorage || storage.Quantity < consumeQuantity {
								err = ecode.AppendMessage(constant.ErrStorageUnavailable,
									fmt.Sprintf("物料%s库存不足", materialRemarks[materialKey].Name))
								return
							}
							storageMap[materialKey].Quantity -= consumeQuantity
						}
					} else {
						if storage, hasStorage := storageMap[d.StorageKey]; !hasStorage || storage.Quantity < d.Quantity {
							err = ecode.AppendMessage(constant.ErrStorageUnavailable,
								fmt.Sprintf("商品%s库存不足", objectRemarks[d.StorageKey].Name))
							return
						}
						storageMap[d.StorageKey].Quantity -= d.Quantity
					}
				} else {
					if storage, hasStorage := storageMap[d.StorageKey]; !hasStorage || storage.Quantity < d.Quantity {
						err = ecode.AppendMessage(constant.ErrStorageUnavailable,
							fmt.Sprintf("物料%s库存不足", materialRemarks[d.StorageKey].Name))
						return
					}
					storageMap[d.StorageKey].Quantity -= d.Quantity
				}
			}
		}
		now := time.Now()
		if orderNumber, err = generator.GenerateOrderNumber(now, constant.OrderTypeLoss, empId); err != nil {
			return
		}
		lossOrder := model.LossOrder{
			OrderNumber: orderNumber,
			WarehouseID: req.Id,
			SgID:        sgId,
			Status:      constant.LossOrderDone,
			DetailCount: uint32(len(req.Details)),
			Creator:     empId,
			CreateTime:  now,
		}
		if err = s.dao.Create(ctx, &lossOrder); err != nil {
			return
		}
		// create details
		var (
			snapshotRemark *model.FullRemark
			remarkJson     json.RawMessage
			priceMap       map[string]uint64
		)
		if priceMap, err = s.dao.GetLastMonthStoragePriceByStorageKeys(ctx, warehouse.ID, storageKeyMap.GetKeys(),
			now, materialRemarks, constant.UnitTypeStorage); err != nil {
			return
		}
		affectedStorageKeyMap := make(util.StringBoolMap)
		affectedMaterialIDMap := make(util.UInt64BoolMap)
		for _, d := range req.Details {
			switch d.StorageType {
			case inventory.StorageType_MATERIAL:
				snapshotRemark = materialRemarks[d.StorageKey]
			case inventory.StorageType_PROD:
				snapshotRemark = objectRemarks[d.StorageKey]
			default:
				err = constant.ErrSystemError
				return
			}
			if remarkJson, err = json.Marshal(&model.LossSnapShotRemark{
				StorageSnapShotRemark: model.StorageSnapShotRemark{
					SnapShotRemark: snapshotRemark.SnapShotRemark,
					StorageUnit:    snapshotRemark.StorageUnit,
					StorageRatio:   snapshotRemark.StorageRatio,
				},
				Memo: d.Memo,
			}); err != nil {
				return
			}
			price, _ := priceMap[d.StorageKey]
			lossOrder.Total += uint64(float64(price) * d.Quantity / snapshotRemark.StorageRatio)
			if err = s.dao.Create(ctx, &model.LossDetail{
				OrderID:     lossOrder.ID,
				WarehouseID: lossOrder.WarehouseID,
				StorageType: uint8(d.StorageType),
				StorageKey:  d.StorageKey,
				Remark:      orm.JSON(remarkJson),
				Quantity:    d.Quantity,
				Price:       price,
				CreateTime:  lossOrder.CreateTime,
			}); err != nil {
				return
			}
			if materialMap, ok := bomMap[d.StorageKey]; ok {
				for materialKey, refQuantity := range materialMap {
					if remarkJson, err = json.Marshal(&model.LossSnapShotRemark{
						StorageSnapShotRemark: model.StorageSnapShotRemark{
							SnapShotRemark: materialRemarks[materialKey].SnapShotRemark,
							StorageUnit:    materialRemarks[materialKey].StorageUnit,
							StorageRatio:   materialRemarks[materialKey].StorageRatio,
						},
						Memo: d.Memo,
					}); err != nil {
						return
					}
					lossQuantity := d.Quantity * refQuantity
					price, _ = priceMap[d.StorageKey]
					if err = s.dao.Create(ctx, &model.LossDetail{
						OrderID:     lossOrder.ID,
						WarehouseID: lossOrder.WarehouseID,
						StorageType: uint8(inventory.StorageType_MATERIAL),
						StorageKey:  materialKey,
						Remark:      orm.JSON(remarkJson),
						Quantity:    lossQuantity,
						Price:       price,
						CreateTime:  lossOrder.CreateTime,
					}); err != nil {
						return
					}
					// 更新日结记录
					if err = s.dao.Create(ctx, &model.StatsDaily{
						RecordDate:    lossOrder.CreateTime,
						ActionType:    constant.ActionLoss,
						WarehouseID:   warehouse.ID,
						StorageKey:    d.StorageKey,
						StorageType:   uint8(d.StorageType),
						StorageName:   materialRemarks[materialKey].Name,
						Quantity:      -lossQuantity,
						Price:         int64(price),
						OperationUnit: materialRemarks[materialKey].StorageUnit,
						Amount:        int64(float64(price) * -lossQuantity / materialRemarks[materialKey].StorageRatio),
					}); err != nil {
						return
					}
					var affected bool
					if affected, err = s.dao.DecreaseStorage(ctx, req.Id, materialKey, lossQuantity, lossOrder.CreateTime); err != nil {
						return
					}
					if affected {
						affectedStorageKeyMap[materialKey] = true
						materialID, _ := strconv.Atoi(materialKey[2:])
						affectedMaterialIDMap[uint64(materialID)] = true
					}
				}
			} else {
				// 更新日结记录
				var remark *model.FullRemark
				switch d.StorageType {
				case inventory.StorageType_MATERIAL:
					remark = materialRemarks[d.StorageKey]
				default:
					remark = objectRemarks[d.StorageKey]
				}
				if err = s.dao.Create(ctx, &model.StatsDaily{
					RecordDate:    lossOrder.CreateTime,
					ActionType:    constant.ActionLoss,
					WarehouseID:   warehouse.ID,
					StorageKey:    d.StorageKey,
					StorageType:   uint8(d.StorageType),
					StorageName:   remark.Name,
					Quantity:      -d.Quantity,
					Price:         int64(price),
					OperationUnit: remark.Unit,
					Amount:        int64(float64(price) * -d.Quantity / remark.StorageRatio),
				}); err != nil {
					return
				}
				var affected bool
				if affected, err = s.dao.DecreaseStorage(ctx, req.Id, d.StorageKey, d.Quantity, lossOrder.CreateTime); err != nil {
					return
				}
				if affected {
					affectedStorageKeyMap[d.StorageKey] = true
				}
			}
		}
		if lossOrder.Total != 0 {
			if err = s.dao.Save(ctx, lossOrder); err != nil {
				return
			}
		}
		return
	})
	return
}
